// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/gtk/gtk_util.h"

#include <dlfcn.h>
#include <gdk/gdk.h>
#include <gtk/gtk.h>
#include <locale.h>
#include <stddef.h>

#include <memory>

#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/debug/leak_annotations.h"
#include "base/environment.h"
#include "base/strings/string_split.h"
#include "base/strings/string_tokenizer.h"
#include "base/strings/string_util.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/gtk/gtk_ui.h"
#include "ui/gtk/gtk_ui_delegate.h"
#include "ui/ozone/public/ozone_platform.h"
#include "ui/views/linux_ui/linux_ui.h"

#if BUILDFLAG(GTK_VERSION) < 4
WEAK_GTK_FN(gtk_widget_path_iter_set_object_name);
WEAK_GTK_FN(gtk_widget_path_iter_set_state);
#endif

namespace gtk {

namespace {

const char kAuraTransientParent[] = "aura-transient-parent";

void CommonInitFromCommandLine(const base::CommandLine& command_line) {
  // Callers should have already called setlocale(LC_ALL, "") and
  // setlocale(LC_NUMERIC, "C") by now. Chrome does this in
  // service_manager::Main.
  DCHECK_EQ(strcmp(setlocale(LC_NUMERIC, nullptr), "C"), 0);
  // This prevent GTK from calling setlocale(LC_ALL, ""), which potentially
  // overwrites the LC_NUMERIC locale to something other than "C".
  gtk_disable_setlocale();
#if GTK_CHECK_VERSION(3, 90, 0)
  gtk_init();
#else
  const std::vector<std::string>& args = command_line.argv();
  int argc = args.size();
  std::unique_ptr<char*[]> argv(new char*[argc + 1]);
  for (size_t i = 0; i < args.size(); ++i) {
    // TODO(piman@google.com): can gtk_init modify argv? Just being safe
    // here.
    argv[i] = strdup(args[i].c_str());
  }
  argv[argc] = nullptr;
  char** argv_pointer = argv.get();

  {
    // http://crbug.com/423873
    ANNOTATE_SCOPED_MEMORY_LEAK;
    gtk_init(&argc, &argv_pointer);
  }
  for (size_t i = 0; i < args.size(); ++i) {
    free(argv[i]);
  }
#endif
}

GdkModifierType GetIbusFlags(const ui::KeyEvent& key_event) {
  auto* properties = key_event.properties();
  if (!properties)
    return static_cast<GdkModifierType>(0);
  auto it = properties->find(ui::kPropertyKeyboardIBusFlag);
  DCHECK(it == properties->end() || it->second.size() == 1);
  uint8_t flags = (it != properties->end()) ? it->second[0] : 0;
  return static_cast<GdkModifierType>(flags
                                      << ui::kPropertyKeyboardIBusFlagOffset);
}

#if BUILDFLAG(GTK_VERSION) < 4
float GetDeviceScaleFactor() {
  views::LinuxUI* linux_ui = views::LinuxUI::instance();
  return linux_ui ? linux_ui->GetDeviceScaleFactor() : 1;
}
#endif

GtkCssContext AppendCssNodeToStyleContextImpl(
    GtkCssContext context,
    GType gtype,
    const std::string& name,
    const std::string& object_name,
    const std::vector<std::string>& classes,
    GtkStateFlags state) {
#if BUILDFLAG(GTK_VERSION) >= 4
  // GTK_TYPE_BOX is used instead of GTK_TYPE_WIDGET because:
  // 1. Widgets are abstract and cannot be created directly.
  // 2. The widget must be a container type so that it unrefs child widgets
  //    on destruction.
  auto* widget_object = object_name.empty()
                            ? g_object_new(GTK_TYPE_BOX, nullptr)
                            : g_object_new(GTK_TYPE_BOX, "css-name",
                                           object_name.c_str(), nullptr);
  auto widget = TakeGObject(GTK_WIDGET(widget_object));

  if (!name.empty())
    gtk_widget_set_name(widget, name.c_str());

  std::vector<const char*> css_classes;
  css_classes.reserve(classes.size() + 1);
  for (const auto& css_class : classes)
    css_classes.push_back(css_class.c_str());
  css_classes.push_back(nullptr);
  gtk_widget_set_css_classes(widget, css_classes.data());

  gtk_widget_set_state_flags(widget, state, false);

  if (context)
    gtk_widget_set_parent(widget, context);
  return GtkCssContext(widget, context ? context.root() : widget);
#else
  GtkWidgetPath* path =
      context ? gtk_widget_path_copy(gtk_style_context_get_path(context))
              : gtk_widget_path_new();
  gtk_widget_path_append_type(path, gtype);

  if (!object_name.empty()) {
    if (GtkCheckVersion(3, 20)) {
      DCHECK(gtk_widget_path_iter_set_object_name);
      gtk_widget_path_iter_set_object_name(path, -1, object_name.c_str());
    } else {
      gtk_widget_path_iter_add_class(path, -1, object_name.c_str());
    }
  }

  if (!name.empty())
    gtk_widget_path_iter_set_name(path, -1, name.c_str());

  for (const auto& css_class : classes)
    gtk_widget_path_iter_add_class(path, -1, css_class.c_str());

  if (GtkCheckVersion(3, 14)) {
    DCHECK(gtk_widget_path_iter_set_state);
    gtk_widget_path_iter_set_state(path, -1, state);
  }

  GtkCssContext child_context(TakeGObject(gtk_style_context_new()));
  gtk_style_context_set_path(child_context, path);
  if (GtkCheckVersion(3, 14)) {
    gtk_style_context_set_state(child_context, state);
  } else {
    GtkStateFlags child_state = state;
    if (context) {
      child_state = static_cast<GtkStateFlags>(
          child_state | gtk_style_context_get_state(context));
    }
    gtk_style_context_set_state(child_context, child_state);
  }
  gtk_style_context_set_scale(child_context, std::ceil(GetDeviceScaleFactor()));
  gtk_style_context_set_parent(child_context, context);
  gtk_widget_path_unref(path);
  return GtkCssContext(child_context);
#endif
}

GtkWidget* CreateDummyWindow() {
#if BUILDFLAG(GTK_VERSION) >= 4
  GtkWidget* window = gtk_window_new();
#else
  GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
#endif
  gtk_widget_realize(window);
  return window;
}

}  // namespace

void GtkInitFromCommandLine(const base::CommandLine& command_line) {
  CommonInitFromCommandLine(command_line);
}

void SetGtkTransientForAura(GtkWidget* dialog, aura::Window* parent) {
  if (!parent || !parent->GetHost())
    return;

  gtk_widget_realize(dialog);
#if BUILDFLAG(GTK_VERSION) >= 4
  GdkWindow* gdk_window = gtk_native_get_surface(gtk_widget_get_native(dialog));
#else
  GdkWindow* gdk_window = gtk_widget_get_window(dialog);
#endif
  gfx::AcceleratedWidget parent_id = parent->GetHost()->GetAcceleratedWidget();
  GtkUi::GetDelegate()->SetGdkWindowTransientFor(gdk_window, parent_id);

  // We also set the |parent| as a property of |dialog|, so that we can unlink
  // the two later.
  g_object_set_data(G_OBJECT(dialog), kAuraTransientParent, parent);
}

aura::Window* GetAuraTransientParent(GtkWidget* dialog) {
  return reinterpret_cast<aura::Window*>(
      g_object_get_data(G_OBJECT(dialog), kAuraTransientParent));
}

void ClearAuraTransientParent(GtkWidget* dialog, aura::Window* parent) {
  g_object_set_data(G_OBJECT(dialog), kAuraTransientParent, nullptr);
  GtkUi::GetDelegate()->ClearTransientFor(
      parent->GetHost()->GetAcceleratedWidget());
}

void ParseButtonLayout(const std::string& button_string,
                       std::vector<views::FrameButton>* leading_buttons,
                       std::vector<views::FrameButton>* trailing_buttons) {
  leading_buttons->clear();
  trailing_buttons->clear();
  bool left_side = true;
  base::StringTokenizer tokenizer(button_string, ":,");
  tokenizer.set_options(base::StringTokenizer::RETURN_DELIMS);
  while (tokenizer.GetNext()) {
    if (tokenizer.token_is_delim()) {
      if (*tokenizer.token_begin() == ':')
        left_side = false;
    } else {
      base::StringPiece token = tokenizer.token_piece();
      if (token == "minimize") {
        (left_side ? leading_buttons : trailing_buttons)
            ->push_back(views::FrameButton::kMinimize);
      } else if (token == "maximize") {
        (left_side ? leading_buttons : trailing_buttons)
            ->push_back(views::FrameButton::kMaximize);
      } else if (token == "close") {
        (left_side ? leading_buttons : trailing_buttons)
            ->push_back(views::FrameButton::kClose);
      }
    }
  }
}

CairoSurface::CairoSurface(SkBitmap& bitmap)
    : surface_(cairo_image_surface_create_for_data(
          static_cast<unsigned char*>(bitmap.getAddr(0, 0)),
          CAIRO_FORMAT_ARGB32,
          bitmap.width(),
          bitmap.height(),
          cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, bitmap.width()))),
      cairo_(cairo_create(surface_)) {}

CairoSurface::CairoSurface(const gfx::Size& size)
    : surface_(cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
                                          size.width(),
                                          size.height())),
      cairo_(cairo_create(surface_)) {
  DCHECK(cairo_surface_status(surface_) == CAIRO_STATUS_SUCCESS);
  // Clear the surface.
  cairo_save(cairo_);
  cairo_set_source_rgba(cairo_, 0, 0, 0, 0);
  cairo_set_operator(cairo_, CAIRO_OPERATOR_SOURCE);
  cairo_paint(cairo_);
  cairo_restore(cairo_);
}

CairoSurface::~CairoSurface() {
  cairo_destroy(cairo_);
  cairo_surface_destroy(surface_);
}

SkColor CairoSurface::GetAveragePixelValue(bool frame) {
  cairo_surface_flush(surface_);
  SkColor* data =
      reinterpret_cast<SkColor*>(cairo_image_surface_get_data(surface_));
  int width = cairo_image_surface_get_width(surface_);
  int height = cairo_image_surface_get_height(surface_);
  DCHECK(4 * width == cairo_image_surface_get_stride(surface_));
  long a = 0, r = 0, g = 0, b = 0;
  unsigned int max_alpha = 0;
  for (int i = 0; i < width * height; i++) {
    SkColor color = data[i];
    max_alpha = std::max(SkColorGetA(color), max_alpha);
    a += SkColorGetA(color);
    r += SkColorGetR(color);
    g += SkColorGetG(color);
    b += SkColorGetB(color);
  }
  if (a == 0)
    return SK_ColorTRANSPARENT;
  return SkColorSetARGB(frame ? max_alpha : a / (width * height), r * 255 / a,
                        g * 255 / a, b * 255 / a);
}

bool GtkCheckVersion(int major, int minor, int micro) {
  static auto version =
      std::make_tuple(gtk_get_major_version(), gtk_get_minor_version(),
                      gtk_get_micro_version());
  return version >= std::make_tuple(major, minor, micro);
}

#if BUILDFLAG(GTK_VERSION) >= 4
GtkCssContext::GtkCssContext(GtkWidget* widget, GtkWidget* root)
    : widget_(widget), root_(WrapGObject(root)) {}
#else
GtkCssContext::GtkCssContext(GtkStyleContext* context)
    : context_(WrapGObject(context)) {}
#endif
GtkCssContext::GtkCssContext() = default;
GtkCssContext::GtkCssContext(const GtkCssContext&) = default;
GtkCssContext::GtkCssContext(GtkCssContext&&) = default;
GtkCssContext& GtkCssContext::operator=(const GtkCssContext&) = default;
GtkCssContext& GtkCssContext::operator=(GtkCssContext&&) = default;
GtkCssContext::~GtkCssContext() = default;

GtkStateFlags StateToStateFlags(ui::NativeTheme::State state) {
  switch (state) {
    case ui::NativeTheme::kDisabled:
      return GTK_STATE_FLAG_INSENSITIVE;
    case ui::NativeTheme::kHovered:
      return GTK_STATE_FLAG_PRELIGHT;
    case ui::NativeTheme::kNormal:
      return GTK_STATE_FLAG_NORMAL;
    case ui::NativeTheme::kPressed:
      return static_cast<GtkStateFlags>(GTK_STATE_FLAG_PRELIGHT |
                                        GTK_STATE_FLAG_ACTIVE);
    default:
      NOTREACHED();
      return GTK_STATE_FLAG_NORMAL;
  }
}

SkColor GdkRgbaToSkColor(const GdkRGBA& color) {
  return SkColorSetARGB(color.alpha * 255, color.red * 255, color.green * 255,
                        color.blue * 255);
}

NO_SANITIZE("cfi-icall")
GtkCssContext AppendCssNodeToStyleContext(GtkCssContext context,
                                          const std::string& css_node) {
  enum {
    CSS_TYPE,
    CSS_NAME,
    CSS_OBJECT_NAME,
    CSS_CLASS,
    CSS_PSEUDOCLASS,
    CSS_NONE,
  } part_type = CSS_TYPE;

  static const struct {
    const char* name;
    GtkStateFlags state_flag;
  } pseudo_classes[] = {
      {"active", GTK_STATE_FLAG_ACTIVE},
      {"hover", GTK_STATE_FLAG_PRELIGHT},
      {"selected", GTK_STATE_FLAG_SELECTED},
      {"disabled", GTK_STATE_FLAG_INSENSITIVE},
      {"indeterminate", GTK_STATE_FLAG_INCONSISTENT},
      {"focus", GTK_STATE_FLAG_FOCUSED},
      {"backdrop", GTK_STATE_FLAG_BACKDROP},
      {"link", GTK_STATE_FLAG_LINK},
      {"visited", GTK_STATE_FLAG_VISITED},
      {"checked", GTK_STATE_FLAG_CHECKED},
  };

  GType gtype = G_TYPE_NONE;
  std::string name;
  std::string object_name;
  std::vector<std::string> classes;
  GtkStateFlags state = GTK_STATE_FLAG_NORMAL;

  base::StringTokenizer t(css_node, ".:#()");
  t.set_options(base::StringTokenizer::RETURN_DELIMS);
  while (t.GetNext()) {
    if (t.token_is_delim()) {
      switch (*t.token_begin()) {
        case '(':
          part_type = CSS_NAME;
          break;
        case ')':
          part_type = CSS_NONE;
          break;
        case '#':
          part_type = CSS_OBJECT_NAME;
          break;
        case '.':
          part_type = CSS_CLASS;
          break;
        case ':':
          part_type = CSS_PSEUDOCLASS;
          break;
        default:
          NOTREACHED();
      }
    } else {
      switch (part_type) {
        case CSS_NAME:
          name = t.token();
          break;
        case CSS_OBJECT_NAME:
          object_name = t.token();
          break;
        case CSS_TYPE: {
#if BUILDFLAG(GTK_VERSION) < 4
          gtype = g_type_from_name(t.token().c_str());
          DCHECK(gtype);
#endif
          break;
        }
        case CSS_CLASS:
          classes.push_back(t.token());
          break;
        case CSS_PSEUDOCLASS: {
          GtkStateFlags state_flag = GTK_STATE_FLAG_NORMAL;
          for (const auto& pseudo_class_entry : pseudo_classes) {
            if (strcmp(pseudo_class_entry.name, t.token().c_str()) == 0) {
              state_flag = pseudo_class_entry.state_flag;
              break;
            }
          }
          state = static_cast<GtkStateFlags>(state | state_flag);
          break;
        }
        case CSS_NONE:
          NOTREACHED();
      }
    }
  }

  // Always add a "chromium" class so that themes can style chromium
  // widgets specially if they want to.
  classes.push_back("chromium");

  return AppendCssNodeToStyleContextImpl(context, gtype, name, object_name,
                                         classes, state);
}

GtkCssContext GetStyleContextFromCss(const std::string& css_selector) {
  // Prepend a window node to the selector since all widgets must live
  // in a window, but we don't want to specify that every time.
  auto context = AppendCssNodeToStyleContext({}, "GtkWindow#window.background");

  for (const auto& widget_type :
       base::SplitString(css_selector, base::kWhitespaceASCII,
                         base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY)) {
    context = AppendCssNodeToStyleContext(context, widget_type);
  }
  return context;
}

SkColor GetFgColorFromStyleContext(GtkCssContext context) {
  GdkRGBA color;
#if GTK_CHECK_VERSION(3, 90, 0)
  gtk_style_context_get_color(context, &color);
#else
  gtk_style_context_get_color(context, gtk_style_context_get_state(context),
                              &color);
#endif
  return GdkRgbaToSkColor(color);
}

SkColor GetBgColorFromStyleContext(GtkCssContext context) {
  // Backgrounds are more general than solid colors (eg. gradients),
  // but chromium requires us to boil this down to one color.  We
  // cannot use the background-color here because some themes leave it
  // set to a garbage color because a background-image will cover it
  // anyway.  So we instead render the background into a 24x24 bitmap,
  // removing any borders, and hope that we get a good color.
  ApplyCssToContext(context,
                    "* {"
                    "border-radius: 0px;"
                    "border-style: none;"
                    "box-shadow: none;"
                    "}");
  gfx::Size size(24, 24);
  CairoSurface surface(size);
  RenderBackground(size, surface.cairo(), context);
  return surface.GetAveragePixelValue(false);
}

SkColor GetFgColor(const std::string& css_selector) {
  return GetFgColorFromStyleContext(GetStyleContextFromCss(css_selector));
}

ScopedCssProvider GetCssProvider(const std::string& css) {
  auto provider = TakeGObject(gtk_css_provider_new());
#if GTK_CHECK_VERSION(3, 90, 0)
  gtk_css_provider_load_from_data(provider, css.c_str(), -1);
#else
  GError* error = nullptr;
  gtk_css_provider_load_from_data(provider, css.c_str(), -1, &error);
  DCHECK(!error);
#endif
  return provider;
}

void ApplyCssProviderToContext(GtkCssContext context,
                               GtkCssProvider* provider) {
  while (context) {
    gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider),
                                   G_MAXUINT);
    context = context.GetParent();
  }
}

void ApplyCssToContext(GtkCssContext context, const std::string& css) {
  auto provider = GetCssProvider(css);
  ApplyCssProviderToContext(context, provider);
}

void RenderBackground(const gfx::Size& size,
                      cairo_t* cr,
                      GtkCssContext context) {
  if (!context)
    return;
  RenderBackground(size, cr, context.GetParent());
  gtk_render_background(context, cr, 0, 0, size.width(), size.height());
}

SkColor GetBgColor(const std::string& css_selector) {
  return GetBgColorFromStyleContext(GetStyleContextFromCss(css_selector));
}

SkColor GetBorderColor(const std::string& css_selector) {
  // Borders have the same issue as backgrounds, due to the
  // border-image property.
  auto context = GetStyleContextFromCss(css_selector);
  gfx::Size size(24, 24);
  CairoSurface surface(size);
  gtk_render_frame(context, surface.cairo(), 0, 0, size.width(), size.height());
  return surface.GetAveragePixelValue(true);
}

SkColor GetSelectionBgColor(const std::string& css_selector) {
  auto context = GetStyleContextFromCss(css_selector);
  if (GtkCheckVersion(3, 20))
    return GetBgColorFromStyleContext(context);
#if BUILDFLAG(GTK_VERSION) < 4
  // This is verbatim how Gtk gets the selection color on versions before 3.20.
  GdkRGBA selection_color;
  G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
  gtk_style_context_get_background_color(
      context, gtk_style_context_get_state(context), &selection_color);
  G_GNUC_END_IGNORE_DEPRECATIONS;
  return GdkRgbaToSkColor(selection_color);
#else
  NOTREACHED();
  return SK_ColorRED;
#endif
}

bool ContextHasClass(GtkCssContext context, const std::string& style_class) {
#if BUILDFLAG(GTK_VERSION) >= 4
  return gtk_style_context_has_class(context, style_class.c_str());
#else
  return gtk_style_context_has_class(context, style_class.c_str()) ||
         gtk_widget_path_iter_has_class(gtk_style_context_get_path(context), -1,
                                        style_class.c_str());
#endif
}

SkColor GetSeparatorColor(const std::string& css_selector) {
  if (!GtkCheckVersion(3, 20))
    return GetFgColor(css_selector);

  auto context = GetStyleContextFromCss(css_selector);
  bool horizontal = ContextHasClass(context, "horizontal");

  int w = 1, h = 1;
  GtkBorder border, padding;
#if GTK_CHECK_VERSION(3, 90, 0)
  auto size = GetSeparatorSize(horizontal);
  w = size.width();
  h = size.height();
  gtk_style_context_get_border(context, &border);
  gtk_style_context_get_padding(context, &padding);
#else
  gtk_style_context_get(context, gtk_style_context_get_state(context),
                        "min-width", &w, "min-height", &h, nullptr);
  GtkStateFlags state = gtk_style_context_get_state(context);
  gtk_style_context_get_border(context, state, &border);
  gtk_style_context_get_padding(context, state, &padding);
#endif
  w += border.left + padding.left + padding.right + border.right;
  h += border.top + padding.top + padding.bottom + border.bottom;

  if (horizontal) {
    w = 24;
    h = std::max(h, 1);
  } else {
    DCHECK(ContextHasClass(context, "vertical"));
    h = 24;
    w = std::max(w, 1);
  }

  CairoSurface surface(gfx::Size(w, h));
  gtk_render_background(context, surface.cairo(), 0, 0, w, h);
  gtk_render_frame(context, surface.cairo(), 0, 0, w, h);
  return surface.GetAveragePixelValue(false);
}

std::string GetGtkSettingsStringProperty(GtkSettings* settings,
                                         const gchar* prop_name) {
  GValue layout = G_VALUE_INIT;
  g_value_init(&layout, G_TYPE_STRING);
  g_object_get_property(G_OBJECT(settings), prop_name, &layout);
  DCHECK(G_VALUE_HOLDS_STRING(&layout));
  std::string prop_value(g_value_get_string(&layout));
  g_value_unset(&layout);
  return prop_value;
}

int BuildXkbStateFromGdkEvent(unsigned int state, unsigned char group) {
  return state | ((group & 0x3) << 13);
}

int GetKeyEventProperty(const ui::KeyEvent& key_event,
                        const char* property_key) {
  auto* properties = key_event.properties();
  if (!properties)
    return 0;
  auto it = properties->find(property_key);
  DCHECK(it == properties->end() || it->second.size() == 1);
  return (it != properties->end()) ? it->second[0] : 0;
}

GdkModifierType GetGdkKeyEventState(const ui::KeyEvent& key_event) {
  // ui::KeyEvent uses a normalized modifier state which is not respected by
  // Gtk, so we need to get the state from the display backend. Gtk instead
  // follows the X11 spec in which the state of a key event is expected to be
  // the mask of modifier keys _prior_ to this event. Some IMEs rely on this
  // behavior. See https://crbug.com/1086946#c11.

  GdkModifierType state = GetIbusFlags(key_event);
  if (key_event.key_code() != ui::VKEY_PROCESSKEY) {
    // This is an synthetized event when |key_code| is VKEY_PROCESSKEY.
    // In such a case there is no event being dispatching in the display
    // backend.
    state = static_cast<GdkModifierType>(
        state | ui::GtkUiDelegate::instance()->GetGdkKeyState());
  }

  return state;
}

#if BUILDFLAG(GTK_VERSION) < 4
GdkEvent* GdkEventFromKeyEvent(const ui::KeyEvent& key_event) {
  GdkEventType event_type =
      key_event.type() == ui::ET_KEY_PRESSED ? GDK_KEY_PRESS : GDK_KEY_RELEASE;
  auto event_time = key_event.time_stamp() - base::TimeTicks();
  int hw_code = GetKeyEventProperty(key_event, ui::kPropertyKeyboardHwKeyCode);
  int group = GetKeyEventProperty(key_event, ui::kPropertyKeyboardGroup);

  // Get GdkKeymap
  GdkKeymap* keymap = GtkUi::GetDelegate()->GetGdkKeymap();

  // Get keyval and state
  GdkModifierType state = GetGdkKeyEventState(key_event);
  guint keyval = GDK_KEY_VoidSymbol;
  GdkModifierType consumed;
  gdk_keymap_translate_keyboard_state(keymap, hw_code, state, group, &keyval,
                                      nullptr, nullptr, &consumed);
  gdk_keymap_add_virtual_modifiers(keymap, &state);
  DCHECK(keyval != GDK_KEY_VoidSymbol);

  // Build GdkEvent
  GdkEvent* gdk_event = gdk_event_new(event_type);
  gdk_event->type = event_type;
  gdk_event->key.time = event_time.InMilliseconds();
  gdk_event->key.hardware_keycode = hw_code;
  gdk_event->key.keyval = keyval;
  gdk_event->key.state = BuildXkbStateFromGdkEvent(state, group);
  gdk_event->key.group = group;
  gdk_event->key.send_event = key_event.flags() & ui::EF_FINAL;
  gdk_event->key.is_modifier = state & GDK_MODIFIER_MASK;
  gdk_event->key.length = 0;
  gdk_event->key.string = nullptr;

  return gdk_event;
}
#endif

GtkIconTheme* GetDefaultIconTheme() {
#if GTK_CHECK_VERSION(3, 90, 0)
  return gtk_icon_theme_get_for_display(gdk_display_get_default());
#else
  return gtk_icon_theme_get_default();
#endif
}

void GtkWindowDestroy(GtkWidget* widget) {
#if GTK_CHECK_VERSION(3, 98, 4)
  gtk_window_destroy(GTK_WINDOW(widget));
#else
  gtk_widget_destroy(widget);
#endif
}

GtkWidget* GetDummyWindow() {
  static GtkWidget* window = CreateDummyWindow();
  return window;
}

gfx::Size GetSeparatorSize(bool horizontal) {
  auto widget = TakeGObject(gtk_separator_new(
      horizontal ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL));
  GtkRequisition natural_size;
  gtk_widget_get_preferred_size(widget, nullptr, &natural_size);
  return {natural_size.width, natural_size.height};
}

}  // namespace gtk
